// Copyright (c) 2021 Presto Labs Pte. Ltd.
// Author: daniel

#include "coin2/exchange/lmax_digital/order_v1/parser.h"

#include <memory>
#include <set>
#include <string>
#include <utility>
#include <vector>

#include <rapidjson/document.h>

#include "coin/proto/coin_query.pb.h"
#include "coin2/base/conversion.h"
#include "coin2/base/log.h"
#include "coin2/exchange/util/json_util.h"
#include "quickfix/Message.h"

namespace coin2::exchange::lmax_digital::order_v1 {

namespace {

using coin::proto::AccountBalance;
using coin::proto::AccountPosition;
using coin::proto::CancelOrderResponse;
using coin::proto::FillType;
using coin::proto::ProductFillBundle;
using coin::proto::ProductOrderBundle;
using coin::proto::ProductOrderElement;
using coin::proto::SubmitOrderResponse;

using base::order::IOrderContext;
using symbology::LmaxdigitalCurrency;
using symbology::LmaxdigitalProduct;

using JsonValue = rapidjson::GenericValue<rapidjson::UTF8<>>;

bool IsLmaxdigitalErrMessage(const JsonValue& data) {
  std::string error_code;
  if (data.HasMember("error_code")) {
    error_code = data["error_code"].GetString();
  }
  return !error_code.empty();
}

inline int64_t GetProcOrderId(const JsonValue& data) {
  int64_t proc_order_id = 0;
  if (data.HasMember("originalInstructionId")) {
    proc_order_id = data["originalInstructionId"].GetInt64();
  } else if (data.HasMember("instructionId")) {
    proc_order_id = data["instructionId"].GetInt64();
  }
  return proc_order_id;
}

inline std::string GetProcOrderId(const std::string& client_id_str) {
  if (client_id_str.empty()) {
    return client_id_str;
  }
  if (client_id_str.find_first_not_of("1234567890") != std::string::npos) {
    LOG(ERROR) << "A non-numberic proc order id found: " << client_id_str;
    return std::string{};
  }
  return client_id_str;
}

inline coin::proto::OrderSide GetOrderSide(const std::string& side) {
  if (side == "buy") {
    return coin::proto::OrderSide::BUY_ORDER;
  } else if (side == "sell") {
    return coin::proto::OrderSide::SELL_ORDER;
  } else {
    return coin::proto::OrderSide::UNKNOWN_ORDER_SIDE;
  }
}

inline coin::proto::OrderType GetOrderType(const std::string& type) {
  // TODO(daniel):
  if (type == "limit") {
    return coin::proto::OrderType::LIMIT_ORDER;
  } else if (type == "market") {
    return coin::proto::OrderType::MARKET_ORDER;
  } else {
    return coin::proto::OrderType::UNKNOWN_ORDER_TYPE;
  }
}

inline void SetOrderStatus(const JsonValue& data, ProductOrderElement* order) {
  // TODO(daniel):
  std::string status = data["workingState"].GetString();
  if (status == "REJECTED") {
    order->set_is_live(false);
  } else if (status == "ACCEPTED") {
    order->set_is_live(true);
  } else if (status == "done") {
    std::string done_reason = data["done_reason"].GetString();
    if (done_reason == "canceled") {
      order->set_cancel_confirmed(true);
    } else if (status == "filled") {
      order->set_fully_filled(true);
    }
  } else if (status == "pending") {
    // linchuan: pending may received after open, so do nothing.
  } else {
    LOG(ERROR) << "[ParseRestOrderInfo] Unknown order status! " << status;
  }
}

}  // namespace

void LmaxdigitalParser::ParseRestAccountBalance() {
  JsonDocument doc;
  if (!PrepareJsonDocument(&doc)) {
    LOG(ERROR) << "[ParseRestAccountBalance] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  auto account_balance = ParseBalance(doc);
  if (!account_balance.has_exchange() || !account_balance.has_market_type()) {
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }
  result_.set_type(MsgType::ACCOUNT_BALANCE);
  *result_.mutable_account_balance() = std::move(account_balance);
}

// NOTE: when field exchange and market_type are set, which means parsed successfully
AccountBalance LmaxdigitalParser::ParseBalance(const JsonValue& doc) {
  RawBalanceMap raw_balance_map;
  auto success = ParseRawBalance(doc, &raw_balance_map);
  if (!success) {
    return {};
  }

  AccountBalance acc_balance;
  acc_balance.set_market_type("Spot");
  acc_balance.set_exchange("Lmaxdigital");
  for (const auto& native_currency : symcache_->GetCurrenciesNativeUpper()) {
    auto* balance = acc_balance.add_each_balance();
    if (raw_balance_map.count(native_currency) == 0) {
      balance->set_total(0);
      balance->set_hold(0);
      balance->set_available(0);
    } else {
      balance->MergeFrom(raw_balance_map.at(native_currency));
    }
    auto* currency = symcache_->GetCurrencyByNative(native_currency);
    balance->set_currency(currency->currency());
    balance->set_currency_native(currency->native_currency());
    DLOG(INFO) << "BALANCE: " << currency->native_currency() << " " << balance->total() << " "
               << balance->available();
  }

  return acc_balance;
}

bool LmaxdigitalParser::ParseRawBalance(const JsonValue& doc, RawBalanceMap* raw_balance_map) {
  /*
  {
    "wallets": [
      {
        "currency": "BTC",
        "cash": 0.03,
        "credit": 0,
        "balance": 0.03
      },
      {
        "currency": "USD",
        "cash": -1899.09,
        "credit": 500000,
        "balance": 498100.91
      }
    ]
  }
  */
  if (!doc.IsObject() || !doc.HasMember("wallets") || !doc["wallets"].IsArray()) {
    LOG(ERROR) << "[ParseBalance] " << JsonToString(doc);
    return false;
  }
  for (const auto& data : doc["wallets"].GetArray()) {
    if (!data.HasMember("currency") || !data.HasMember("cash") || !data.HasMember("credit") ||
        !data.HasMember("balance")) {
      LOG(ERROR) << "[ParseBalance] empty wallet found. " << JsonToString(doc);
      continue;
    }
    CurrencyBalance balance;
    balance.set_currency_native(data["currency"].GetString());
    double current_total = stod(data["cash"].GetString());
    balance.set_available(current_total);
    balance.set_total(current_total);
    balance.set_hold(0);
    raw_balance_map->emplace(balance.currency_native(), balance);
  }

  return true;
}

ProductOrderElement LmaxdigitalParser::ParseRestOrderInfo(const JsonValue& data) {
  /*
  [
    {
      "timeInForce":"GoodForDay",
      "instructionId":3708150461000007,
      "originalInstructionId":3708150461000007,
      "orderId":"AAAEEAAAAAAiS+cg",
      "accountId":2127979254,
      "instrumentId":5001,
      "instrument":"BTCUSD",
      "price":15000,
      "quantity":10,
      "matchedQuantity":0,
      "matchedCost":0,
      "cancelledQuantity":0,
      "timestamp":"2020-10-26T10:29:11.000Z",
      "orderType":"STOP_COMPOUND_PRICE_LIMIT",
      "openQuantity":0,
      "openCost":0,
      "cumulativeCost":0,
      "commission":0,
      "stopReferencePrice":15000,
      "stopLossOffset":"",
      "stopProfitOffset":"",
      "workingState":"ACCEPTED"
    },
  ]
  */
  ProductOrderElement order;
  order.set_market_type(MarketType::Spot);
  order.set_exchange_type(ExchangeType::Lmaxdigital);
  order.set_api_version("v1");
  order.set_exchange_order_id(data["orderId"].GetString());
  order.set_symbol(data["instrument"].GetString());
  order.set_price(stod(data["price"].GetString()));
  order.set_qty(stod(data["quantity"].GetString()));
  order.set_proc_order_id(GetProcOrderId(data));
  // TODO(daniel): to confirm
  // order.set_side(GetOrderSide(data["side"].GetString()));
  order.set_order_type(GetOrderType(data["orderType"].GetString()));
  order.set_timestamp(GetCurrentTimestamp());
  SetOrderStatus(data, &order);
  return order;
}

ProductOrderBundle LmaxdigitalParser::ParseRestOrderInfoList(const JsonValue& data) {
  ProductOrderBundle product_order_bundle;
  if (!data.IsObject() || !data.HasMember("orders") || !data["orders"].IsArray()) {
    LOG(ERROR) << "[ParseRestOrderInfoList] " << JsonToString(data);
    return product_order_bundle;
  }

  for (const auto& info : data["orders"].GetArray()) {
    auto order = product_order_bundle.add_each_order();
    auto new_order = ParseRestOrderInfo(info);
    auto oc = GetOcm()->FindByExchangeOrderId(new_order.exchange_order_id());
    // If order_context state is dead but exchange order info shows it is alive,
    // print warning and ignore.
    if (oc && (!oc->is_alive()) && new_order.is_live()) {
      // Ingore this order order info if order status is not consistent.
      LOG_EVERY_N(ERROR, 10) << "Lmaxdigital wrong rest open order list: \n"
                             << JsonToString(data) << "\n"
                             << new_order.DebugString();
      continue;
    }
    order->MergeFrom(new_order);
  }
  return product_order_bundle;
}

void LmaxdigitalParser::ParseFixMessage(const RawMessage& msg) {
  result_.Clear();
  if (msg.fix_message == nullptr) {
    LOG(ERROR) << "fix message is empty";
    return;
  }
  const FIX::Message& message = *msg.fix_message;

  DLOG(INFO) << message;

  FIX::MsgType msg_type;
  if (!message.getHeader().getFieldIfSet(msg_type)) {
    return;
  }
  if (MSG_TYPE_SESSION_REJECT == msg_type) {
    // TODO(daniel) - session reject
  } else if (MSG_TYPE_EXECUTION_REPORT == msg_type) {
    ParseFixSubmitResponse(message);
    ParseFixCancelResponse(message);
    ParseFixFillInfo(message);
    ParseFixOrderInfo(message);
  } else if (MSG_TYPE_ORDER_CANCEL_REJECT == msg_type) {
    ParseFixCancelResponse(message);
  }
}

void LmaxdigitalParser::ParseFixSubmitResponse(const FIX::Message& message) {
  // handle order rejected
  FIX::ExecType exec_type;
  if (!message.getFieldIfSet(exec_type)) {
    return;
  }
  if (EXEC_TYPE_REJECTED != exec_type) {
    return;
  }
  std::optional<int64_t> proc_order_id;
  FIX::ClOrdID client_id;
  if (message.getFieldIfSet(client_id)) {
    proc_order_id = ToInt64(GetProcOrderId(client_id.getValue()));
  }
  if (!proc_order_id) {
    LOG(ERROR) << "proc_order_id not found. " << message;
    return;
  }
  FIX::Symbol symbol;
  message.getFieldIfSet(symbol);

  SubmitOrderResponse& resp = *result_.mutable_submit_response();
  resp.Clear();
  resp.set_symbol(symbol.getValue());
  resp.set_proc_order_id(*proc_order_id);
  resp.set_timestamp(GetCurrentTimestamp());
  resp.set_error_code(coin::proto::OrderErrorCode::ORDER_UNKNOWN_ERROR);
  FIX::OrdRejReason error_code;
  if (message.getFieldIfSet(error_code)) {
    if (0 == error_code) {
      // BROKER OPTION
    } else if (1 == error_code) {
      // UNKNOWN SYMBOL
    } else if (2 == error_code) {
      // EXCHANGE CLOSED
    } else if (5 == error_code) {
      // UNKNOWN ORDER
    } else if (6 == error_code) {
      // DUPLICATE ORDER
    }
    resp.set_exchange_error_code(std::to_string(error_code));
  }
  FIX::Text error_reason;
  if (message.getFieldIfSet(error_reason)) {
    if ("RISK_LIMIT_CANNOT_SELL" == error_reason || "RISK_LIMIT_CANNOT_BUY" == error_reason ||
        "RISK_LIMIT_CANNOT_TRADE" == error_reason) {
      resp.set_error_code(OrderErrorCode::SUBMIT_ORDER_INSUFFICIENT_FUND);
    }
    resp.set_exchange_error_msg(error_reason.getValue());
  }
  resp.set_success(false);
  result_.set_type(MsgType::SUBMIT_RESPONSE);
}

void LmaxdigitalParser::ParseFixCancelResponse(const FIX::Message& message) {
  FIX::MsgType msg_type;
  if (!message.getHeader().getFieldIfSet(msg_type)) {
    return;
  }
  if (MSG_TYPE_ORDER_CANCEL_REJECT != msg_type) {
    return;
  }
  std::optional<int64_t> proc_order_id;
  FIX::OrigClOrdID client_id;
  if (message.getFieldIfSet(client_id)) {
    proc_order_id = ToInt64(GetProcOrderId(client_id.getValue()));
  }
  if (!proc_order_id) {
    LOG(ERROR) << "proc_order_id not found. " << message;
    return;
  }
  FIX::OrderID order_id;
  message.getFieldIfSet(order_id);

  CancelOrderResponse& resp = *result_.mutable_cancel_response();
  resp.Clear();
  resp.set_proc_order_id(*proc_order_id);
  resp.set_exchange_order_id(order_id.getValue());
  resp.set_timestamp(GetCurrentTimestamp());

  resp.set_error_code(coin::proto::OrderErrorCode::ORDER_UNKNOWN_ERROR);
  FIX::CxlRejReason error_code;
  if (message.getFieldIfSet(error_code)) {
    if (1 == error_code) {
      resp.set_error_code(coin::proto::OrderErrorCode::ORDER_NOT_FOUND);
    } else if (2 == error_code) {
      // TODO(daniel): 2 = Broker option
    } else {
      LOG(WARNING) << "Undocumented error code found: " << error_code
                   << ", fix_message: " << message.toString();
    }
    resp.set_exchange_error_code(std::to_string(error_code));
  }
  FIX::Text error_reason;
  if (message.getFieldIfSet(error_reason)) {
    resp.set_exchange_error_msg(error_reason.getValue());
  }
  resp.set_success(false);
  result_.set_type(MsgType::CANCEL_RESPONSE);
}

void LmaxdigitalParser::ParseFixFillInfo(const FIX::Message& message) {
  FIX::ExecType exec_type;
  if (!message.getFieldIfSet(exec_type)) {
    return;
  }
  if (EXEC_TYPE_PARTIALLY_FILL != exec_type && EXEC_TYPE_FILL != exec_type) {
    return;
  }
  FIX::OrderID order_id;
  FIX::Symbol symbol;
  FIX::LastPx price;
  FIX::LastShares order_qty;
  FIX::ExecID fill_id;
  message.getFieldIfSet(order_id);
  message.getFieldIfSet(symbol);
  message.getFieldIfSet(price);
  message.getFieldIfSet(order_qty);
  message.getFieldIfSet(fill_id);

  ProductFillBundle& product_fill_bundle = *result_.mutable_fill_list();
  product_fill_bundle.Clear();
  auto fill = product_fill_bundle.add_each_fill();
  fill->Clear();
  fill->set_symbol(symbol.getValue());
  fill->set_fill_timestamp(GetCurrentTimestamp());
  fill->set_exchange_order_id(order_id.getValue());
  fill->set_price(price.getValue());
  fill->set_qty(order_qty.getValue());
  fill->set_fill_id(fill_id.getValue());
  FIX::LastLiquidityInd aggressor_ind;
  if (message.getFieldIfSet(aggressor_ind)) {
    auto ind = aggressor_ind.getValue();
    if (1 == ind) {
      fill->set_fill_type(FillType::MAKER_FILL_TYPE);
    } else if (2 == ind) {
      fill->set_fill_type(FillType::TAKER_FILL_TYPE);
    }
  }
  result_.set_type(MsgType::ORDER_FILL);
}

void LmaxdigitalParser::ParseFixOrderInfo(const FIX::Message& message) {
  FIX::ExecType exec_type;
  if (!message.getFieldIfSet(exec_type)) {
    return;
  }
  if (EXEC_TYPE_NEW_ORDER == exec_type || EXEC_TYPE_PARTIALLY_FILL == exec_type ||
      EXEC_TYPE_FILL == exec_type || EXEC_TYPE_CANCELED == exec_type ||
      EXEC_TYPE_REPLACE == exec_type) {
    FIX::OrderID order_id;
    FIX::Symbol symbol;
    FIX::Price price;
    FIX::OrderQty order_qty;
    FIX::Side side;
    FIX::OrdStatus order_status;
    message.getFieldIfSet(order_id);
    message.getFieldIfSet(symbol);
    message.getFieldIfSet(price);
    message.getFieldIfSet(order_qty);
    message.getFieldIfSet(side);
    message.getFieldIfSet(order_status);
    int64_t proc_order_id = 0;
    // TODO(daniel): check EXEC_TYPE_REPLACE
    if (EXEC_TYPE_CANCELED == exec_type) {
      FIX::OrigClOrdID client_id;
      if (message.getFieldIfSet(client_id)) {
        if (auto poi = ToInt64(GetProcOrderId(client_id.getValue())); poi) {
          proc_order_id = *poi;
        }
      }
    } else {
      FIX::ClOrdID client_id;
      if (message.getFieldIfSet(client_id)) {
        if (auto poi = ToInt64(GetProcOrderId(client_id.getValue())); poi) {
          proc_order_id = *poi;
        }
      }
    }

    ProductOrderBundle& product_order_bundle = *result_.mutable_order_list();
    product_order_bundle.Clear();
    auto order = product_order_bundle.add_each_order();
    order->Clear();
    order->set_market_type(MarketType::Spot);
    order->set_exchange_type(ExchangeType::Lmaxdigital);
    order->set_api_version("v1");
    order->set_exchange_order_id(order_id.getValue());
    order->set_proc_order_id(proc_order_id);
    order->set_symbol(symbol.getValue());
    order->set_price(price.getValue());
    order->set_qty(order_qty);

    if (BUY == side.getValue()) {
      order->set_side(coin::proto::OrderSide::BUY_ORDER);
    } else if (SELL == side.getValue()) {
      order->set_side(coin::proto::OrderSide::SELL_ORDER);
    }
    order->set_timestamp(GetCurrentTimestamp());
    switch (order_status) {
      case ORD_STATUS_NEW_ORDER:
      case ORD_STATUS_PARTIALLY_FILLED:
      case ORD_STATUS_REPLACED_ORDER:
        order->set_is_live(true);
        break;
      case ORD_STATUS_FILLED:
        order->set_fully_filled(true);
        break;
      case ORD_STATUS_CANCELLED_ORDER: {
        order->set_cancel_confirmed(true);
        FIX::Text error_reason;
        if (message.getFieldIfSet(error_reason)) {
          if ("RISK_LIMIT_CANNOT_SELL" == error_reason || "RISK_LIMIT_CANNOT_BUY" == error_reason ||
              "RISK_LIMIT_CANNOT_TRADE" == error_reason) {
            LOG(ERROR) << "order canceled due to infufficient funds"
                       << ", exchange_order_id=" << order->exchange_order_id()
                       << ", proc_order_id=" << order->proc_order_id();
          }
        }
        break;
      }
      default:
        LOG(ERROR) << "[ParseFIXOrderInfo] Unknown order status! " << order_status;
        break;
    }
    result_.set_type(MsgType::ORDER_STATUS);
  }
}

auto LmaxdigitalParser::ParseAuthToken(const JsonValue& doc) -> std::string {
  if (!doc.IsObject()) {
    LOG(ERROR) << "[ParseAuthToken] Invalid json object";
    return "";
  }

  if (IsLmaxdigitalErrMessage(doc)) {
    LOG(ERROR) << "[ParseAuthToken] " << JsonToString(doc);
    return "";
  }
  if (!doc.HasMember("token")) {
    LOG(ERROR) << "[ParseAuthToken] Unexpected auth format!" << JsonToString(doc);
    return "";
  }
  std::string token_str = doc["token"].GetString();

  return token_str;
}

void LmaxdigitalParser::ParseRestAuthToken() {
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseRestAuthToken] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }
  auto token = ParseAuthToken(document);
  LOG(INFO) << "token string: " << token;
  if (!token.empty()) {
    mb_->PublishEvent("lmaxdigital_spot/token", &token);
    result_.set_type(MsgType::IGNORABLE);
  } else {
    LOG(ERROR) << "Invalid token response: " << JsonToString(document);
  }
}

auto LmaxdigitalParser::ParseRawMessage(const RawMessage& msg) -> const ParseResult& {
  result_.Clear();
  msg_ = &msg;
  const auto sub_topic_id = static_cast<OrderSubTopicId>(msg.type);

  if (sub_topic_id == OrderSubTopicId::QUICKFIX_MESSAGE) {
    ParseFixMessage(msg);
    return result_;
  }

  // REST_MESSAGE
  switch (msg.http_context->query_type) {
    case OrderSubTopicId::LISTEN_KEY: {
      ParseRestAuthToken();
      break;
    }
    case OrderSubTopicId::REST_OPEN_ORDERS: {
      // from LMAX support, the orders of maker session will not be listed
      // *result_.mutable_order_list() = ParseRestOrderInfoList(document);
      // result_.set_type(MsgType::ORDER_STATUS);
      break;
    }
    case OrderSubTopicId::REST_ACCOUNT_BALANCE: {
      ParseRestAccountBalance();
      break;
    }
    default: {
      LOG(ERROR) << "Parsing Error. " << msg.DebugString();
      break;
    }
  }

  return result_;
}

}  // namespace coin2::exchange::lmax_digital::order_v1
